iT邦幫忙

2

實作login試誤篇

  • 分享至 

  • xImage
  •  

前言

這星期二突然發現我們應該做的登入系統還沒做,聽隊友們討論聽得頭皮發麻,太多我聽都沒聽過的專有名詞,但也只能故作鎮定繼續聽下去,討論結束前,試圖用很具體很直白的方式確認我該做的事後,回到位子上,我其實不知道怎麼開始,先研究了一下什麼是pinia,再研究一下前端登入token,老實說我連token是啥都不知道ㄎㄎ,這天的結束,Tim給了我一個功課:去看一下quasar cookies...點頭後帶著滿滿的問號回家去。
在家看完cookies後,我也不知道這到底用來幹嘛?順便把那堆專有名詞查一查,恩~好像很有趣,雖然有趣,問號不減反增,心裡髒話都要飆出來了,隔天Tim居然一早就出現在工作室,根本一盞明燈啊~~趕快巴著他把問號解一解,他也很好心的帶著我一步一步實作,如果沒有他,我根本毫無頭緒,做完後趁還有記憶時趕快把來龍去脈筆記下來,本來說好的V-model你就再等等嘿!這禮拜救火先...

那就開始吧~

  1. 開新檔案Login.vue,先簡單刻出畫面,form中放兩個input,分別為username及password
            <q-form class="q-gutter-md">
              <q-input
                filled
                v-model="username"
                label="Username"
                lazy-rules
              />

              <q-input
                filled
                type="password"
                v-model="password"
                label="Password"
                lazy-rules
              />

              <div>
                <q-btn label="Login" type="submit" color="primary" />
              </div>
            </q-form>
  1. 以regex做簡單驗證,先設定條件:a.此欄位必填 b.僅可輸入英數字,在q-input標籤內加入以下code
         :rules="[
                  (val) => !!val || `此欄位必填`,
                  (val) => /^[a-zA-Z0-9]*$/.test(val) || `請輸入英數字`,
                ]"
  1. 設監聽器:在submit後執行函式onlogin
  2. 在步驟2&3做完後,template中的html如下:
            <q-form class="q-gutter-md" @submit="onLogin">
              <q-input
                filled
                v-model="username"
                label="Username"
                lazy-rules
                :rules="[
                  (val) => !!val || `此欄位必填`,
                  (val) => /^[a-zA-Z0-9]*$/.test(val) || `請輸入英數字`,
                ]"
              />

              <q-input
                filled
                type="password"
                v-model="password"
                label="Password"
                lazy-rules
                :rules="[
                  (val) => !!val || `此欄位必填`,
                  (val) => /^[a-zA-Z0-9]*$/.test(val) || `請輸入英數字`,
                ]"
              />

              <div>
                <q-btn label="Login" type="submit" color="primary" />
              </div>
            </q-form>
  1. function onLogin內容:取得使用者輸入的data
const username = ref("");
const password = ref("");

async function onLogin() {
    const formData = {
      username: username.value,
      password: password.value,
    };
} 

  1. 畫面頁先到此告一段落,轉開一個新檔案auth.js,執行驗證動作,後端部分隊友已經幫我寫好一隻api,跟他們確認我要做的事應該就是這樣吧?!(滿臉懞樣...)
    • 如果status等於200即驗證過關,存token
    • 如果status不等於200,跳出錯誤訊息,告知使用者帳密錯誤
export const useAuthStore = defineStore("auth", {
  state: () => ({
    token: ref(""),
    errorMessage: ref(""),
  }),
  actions: {
    async login(formData) {
      try {
        const res = await api.post("/auth/login", formData);
        if (res.status === 200) {
          this.token = res.data.token;
        }
      } catch (error) {
        console.log(error.message);
        this.errorMessage = "帳號或密碼有誤";
      }
    },
  1. 天真如我以為上面那樣就存好token了。但根本不是這樣,好的,原來我得用setCookie的方式把token放入cookies裡
export const useAuthStore = defineStore("auth", {
  state: () => ({
    token: ref(""),
    errorMessage: ref(""),
  }),
  actions: {
    async login(formData) {
      try {
        const res = await api.post("/auth/login", formData);
        if (res.status === 200) {
          this.token = res.data.token;
          //加在這裡
          Cookies.set("jwt", this.token); 
          //加在這裡
        }
      } catch (error) {
        console.log(error.message);
        this.errorMessage = "帳號或密碼有誤";
      }
    },
  1. 另外要加一個函式執行從cookie拿token的動作
getTokenFromCookie() {
      this.token = Cookies.get("jwt");
      if (!this.token) throw new Error();
    },
  1. 還有另一個函式執行從cookie中移除token的動作
removeJWTCookie() {
      Cookies.remove("jwt");
      this.token = null
    },
  1. 回到Login.vue,引入useAuthStore,在函式onLogin中加上驗證,如果驗證有過則導入首頁
const authStore = useAuthStore()

async function onLogin() {
    const formData = {
      username: username.value,
      password: password.value,
    };
    await authStore.login(formData);
    router.push("/");
}
  1. 前面login告一段落,要去首頁處理如果使用者cookies帶有JWT,則可直接瀏覽頁面,無需再登入,反之則導入登入頁面
  • 引入useAuthStore
  • 引入onBeforeMount from vue
import { onBeforeMount } from "vue";
import { useAuthStore } from "src/stores/auth";

onBeforeMount(() => {
  try {
    authStore.getTokenFromCookie();
  } catch (error) {
    router.replace("/login");
  }
});
  1. 接著要把token綁在api的request上,讓後端確認身分權限,再根據其權限回傳相對應資料,這一部分要在管理api的檔案axios.js中做處理
api.interceptors.request.use(
  (config) => {
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`;
    }
    return config;
  },

  function (error) {
    // Do something with request error
    return Promise.reject(error);
  }
);
  1. 另外為了判斷JWT真偽,在axios.js先做如果response的status為401時,就清空JWT並導回登入頁的設定備用,待後端邏輯寫好後會用上
api.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error) {
    if (401 === error.response.status) {
      authStore.removeJWTCookie();
      router.replace("/login");
    }
    return Promise.reject(error);
  }
);

目前實作先到此(汗),之後還可以優化加上過期時間及登出功能。

重構

  1. 過了一天後端邏輯寫好來測試步驟13功能,登愣,雖然會清除JWT但不會導回登入頁。

最後在網路上求解,解方如下:
https://ithelp.ithome.com.tw/upload/images/20240126/20163234kc0lw7Ap2K.png
https://ithelp.ithome.com.tw/upload/images/20240126/20163234RYb4E21slq.png

依樣畫葫蘆後成功了,感謝網路大神~之後Tim跟我講解了一下原理,我們直接引用,在畫面渲染前是拿不到router的,所以必須創一個globalRouter在一開始還沒有接到時預設為null,主要畫面渲染後有router時就變為router,再將globalRouter輸出供axios.js引用即可
6. 經過高人指點,發現我一開始用變數存取token的方式並不優,直接用cookies的方式會簡潔許多,所以從步驟6之後做修改

export const useAuthStore = defineStore("auth", {
  state: () => ({
    errorMessage: ref(""),
  }),
  actions: {
    async login(formData) {
      try {
        const res = await api.post("/auth/login", formData);
        if (res.status === 200) {
          Cookies.set("jwt", res.data.token, { expires: 1 });
        }
      } catch (error) {
        console.log(error.message);
        this.errorMessage = "帳號或密碼有誤";
      }
    },
  1. 不再需要這個getTokenfromCookie的函式,改成在axios.js檔中,只要一發出請求就先從cookies取JWT
  api.interceptors.request.use(
  (config) => {
    //加上這行
    const token = Cookies.get("jwt");
    //加上這行
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },

  function (error) {
    return Promise.reject(error);
  }
);
  1. 同理也不需要removeJWTCookie這個函式,直接寫在axios.js中即可
api.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error) {
    if (error.response.status == 401) {
      //修改這一行
      Cookies.remove("jwt")
      //修該這一行
      globalRouter.router.push("/login");
    }
    return Promise.reject(error);
  }
);
  1. 原步驟11則修改如下:
onBeforeMount(() => {
  if (!Cookies.get("jwt")) router.replace("/login");
});

重構到此告一段落,未來如果又發現更好的方式再繼續更新,人生就是不斷地試誤不斷地進步,大家說是吧(壓力解除後有空講廢話了XD)

可能大家都知道但我突然之間領悟的小筆記

  • onBeforeMount:在畫面渲染前執行/onMount:在畫面渲染後執行,所以如果是權限問題應該放在渲染前,而不是渲染後,如果是放在渲染後,渲染完才發現沒有權限看,再導入登入頁,雖然使用者端沒有差別,domain端卻多了一步,很沒必要,可能有耗效能的問題,我不確定,自己猜的...
  • throw的error一定要有個相對應接它的對象,不然會報錯

資料來源


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言